#pragma once

#include <unordered_map>
#include <list>
#include <sstream>

#include <GSLAM/core/TileManager.h>
#include <GSLAM/core/Timer.h>
#include <GSLAM/core/Utils.h>
#include <GSLAM/core/Messenger.h>
#include <GSLAM/core/Glog.h>

#define HAS_LZ4
#ifdef HAS_LZ4
#include <GSLAM/core/Utils_LZ4.h>
#endif

#include "GSLAM/core/TileManagerCached.h"

namespace GSLAM {

class TudouProjection: public MercatorProjection
{
public:
    double left_2000 = -180;
    double top_2000 = 90;


    virtual std::string type(){return "TudouProjection";}
    virtual pi::Point2_<int64_t > fromLatLngToPixel(double lat, double lng, int zoom)
    {
        double resolution = 0.7031249999891485/pow(2,zoom-1);
        return pi::Point2_<int64_t >(floor((lng - left_2000) / resolution),floor((top_2000 - lat) / resolution));
    }

    virtual PointWithLatLng fromPixelToLatLng(int64_t x,int64_t y,int zoom)
    {
        double resolution = 0.7031249999891485/pow(2,zoom-1);
        return PointWithLatLng(top_2000-y*resolution,x*resolution+left_2000);
    }
};

template <typename TileType>
class TileManagerFileSystem: public GSLAM::TileManager
{
public:
    TileManagerFileSystem(std::string saveFolder="")
        :_folder(saveFolder){
        std::ifstream ifs(_folder+"/area.txt");
        if(ifs.is_open()){
            std::string projType;
            ifs>>_area.level>>_area.min>>_area.max>>projType;
            if(projType=="GCJ02Projection")
                _projection=TileProjectionPtr(new GCJ02Projection());
            else if(projType=="BaiduProjection")
                _projection=TileProjectionPtr(new BaiduProjection());
            else if(projType=="TudouProjection")
                _projection=TileProjectionPtr(new TudouProjection());
            else
                _projection=TileProjectionPtr(new MercatorProjection());
        }
    }

    ~TileManagerFileSystem(){

    }

    virtual void setTileArea(TileArea area){
      _area=area;
      std::ofstream ofs(_folder+"/area.txt");
      if(ofs.is_open()){
        ofs<<_area.level<<" "<<_area.min<<" "<<_area.max<<" "<<_projection->type();
      }
    }
    virtual void setProjection(TileProjectionPtr projection){
      _projection=projection;
      std::ofstream ofs(_folder+"/area.txt");
      if(ofs.is_open()){
        ofs<<_area.level<<" "<<_area.min<<" "<<_area.max<<" "<<_projection->type();
      }
    }

    virtual std::string type()const{return "TileManagerFileSystem";}

    virtual void  call(const std::string& command,void* arg=NULL)
    {
        if( command == "renameCacheFolder" )
        {
            std::unique_lock<std::mutex> lock(_mutex);

            // get old & new folder name
            std::string folderNew;
            folderNew = *((std::string*) arg);

            // do rename
            GSLAM::utils::path_rmdir(folderNew);
            int r = GSLAM::utils::path_rename(_folder, folderNew);
            _folder=folderNew;
        }
    }

    virtual TilePtr getTile(int x,int y,int z)
    {
        std::unique_lock<std::mutex> lock(_mutex);
//        GSLAM::ScopedTimer mT("TileManagerFileSystem::getTile");

        std::ifstream ifs(getFilePath(x,y,z),std::ios::in|std::ios::binary);

        if(!ifs.is_open()) return TilePtr();
        TilePtr tile(new TileType());
        if(!tile->fromStream(ifs)) return TilePtr();
        return tile;
    }

    virtual bool    setTile(int x,int y,int z, const TilePtr& tile)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if(!tile) return false;
//        GSLAM::ScopedTimer mT("TileManagerFileSystem::setTile");

        std::ofstream ofs(getFilePath(x,y,z),std::ios::out|std::ios::binary);
        if(!ofs.is_open()) return false;
        return tile->toStream(ofs);
    }

    std::string getFilePath(int x,int y,int z){
        return _folder+"/"+std::to_string(z)
                +"_"+std::to_string(x)
                +"_"+std::to_string(y)+".tile";
    }

    std::string _folder;
    std::mutex  _mutex;
};

template <typename TileType>
class TileManagerFileSystemLZ4: public GSLAM::TileManagerFileSystem<TileType>
{
public:
    TileManagerFileSystemLZ4(std::string saveFolder="")
        :GSLAM::TileManagerFileSystem<TileType>(saveFolder){
    }

    virtual std::string type()const{return "TileManagerFileSystemLZ4";}


    virtual TilePtr getTile(int x,int y,int z)
    {
        std::unique_lock<std::mutex> lock(this->_mutex);

        std::ifstream ifs(this->getFilePath(x,y,z),std::ios::in|std::ios::binary);

        if(!ifs.is_open()) return TilePtr();
        char isCompressed=true;
        ifs.read(&isCompressed,1);
        // Uncompress here
        std::string Inbuf( (std::istreambuf_iterator<char>(ifs)) , std::istreambuf_iterator<char>() );
        if(Inbuf.size()==0) return TilePtr();
        std::stringstream sst;

        if(isCompressed){
            std::vector<char> buf(2e6);
            int buf_len = LZ4_decompress_safe(Inbuf.data(), buf.data(), Inbuf.size(), buf.size());
            sst.str(std::string(buf.data(),buf_len));
        }
        else
            sst.str(Inbuf);

        TilePtr tile(new TileType());
        if(!tile->fromStream(sst))
        {
          LOG(ERROR)<<"Obtained buffer but can't load tile. compressed "<<isCompressed;
          return TilePtr();
        }
//        LOG(ERROR)<<"Success loaded from"<<this->getFilePath(x,y,z)
//                 <<","<<tile->getTileImage().cols<<"X"<<tile->getTileImage().rows
//                <<","<<tile->getTileDem().cols<<"X"<<tile->getTileDem().rows;
        return tile;
    }

    virtual bool    setTile(int x,int y,int z, const TilePtr& tile)
    {
        std::unique_lock<std::mutex> lock(this->_mutex);
        if(!tile) return false;

        std::stringstream sst;
        if(!tile->toStream(sst)) return false;

        std::string buf=sst.str();
        std::vector<char> outBuf(buf.size());
        int outSize=LZ4_compress_default(buf.data(),outBuf.data(),buf.size(),outBuf.size());
        char isCompressed=true;
        if(outSize==0){
            isCompressed=false;
            outBuf=std::vector<char>(buf.begin(),buf.end());
            outSize=buf.size();
        }

        std::ofstream ofs(this->getFilePath(x,y,z),std::ios::out|std::ios::binary);
        if(!ofs.is_open())
        {
            LOG(ERROR)<<"Can't open file "<<this->getFilePath(x,y,z)<<" for write.";
            return false;
        }
        ofs.write(&isCompressed,sizeof(isCompressed));
        ofs.write(outBuf.data(),outSize);
        return true;
    }

    virtual bool save(const std::string& file){
        if(this->maxZoomLevel() <=0) return false;
        int idx=file.find_last_of('.');
        if(idx==std::string::npos) return false;
        std::string ext=file.substr(idx+1);
        if("tif"==ext)
        {
            return saveGDAL(file);
        }
        return false;
    }

    bool saveGDAL(const std::string& file)
    {
      const Jvar tileDB_tif=GSLAM::Registry::load("tileDB_tif");

      if(!tileDB_tif["exportTileManager"].isFunction()){
          LOG(ERROR)<<"Unable to find exportTileManager function";
          return false;
      }

      bool ret = tileDB_tif["save_tif"]((TileManager*)this,file).as<bool>();
      return ret;
    }
};

} // end of namespace GSLAM
